<?php
declare(strict_types=1);

/**
 * 秒杀商品库存扣减服务功能
 */
namespace Service\CommodityService;
use Service\BaseService;
use App\Model\CommodityModel\CommodityStock;
use Service\Code\Normal;
use Service\AppService\IdService;
use Hyperf\DbConnection\Db;
use App\Model\CommodityModel\CommodityStockLog;
use App\Amqp\Producer\StockRedisServiceMessageProducer;
use Hyperf\Utils\ApplicationContext;
use Hyperf\Amqp\Producer;
use App\Model\CommodityModel\CommodityArchives;
class CommodityStockSkillService extends BaseService
{
    const cache_redis_goods_key = 'stockskillgoods:';
    
    const redis_subscribe_queue = 'order_stock';
    
    //库存调整模式（1-增加，2-减少 ，3-全量初始化,4-扣除库存,5-恢复库存）
    const stock_mode_init        = 3;
    const stock_mode_change_incr = 1;
    const stock_mode_change_decr = 2;
    const stock_mode_order_decr  = 4; 
    const stock_mode_order_incr  = 5;
    
    
    /**
     * 查询库存变更日志功能
     * @param array $param
     */
    public function queryStockList(array $param)
    {
        $page   = !empty($param['page']) ? $param['page'] : 1;
        $limit  = !empty($param['size']) ? $param['size'] : 10;
        $page   = (int)$page;
        $limit  = (int)$limit;
        $table = CommodityStock::instance()->getTable();
        $query = CommodityStock::instance()
        ->from("{$table} as k")
        ->select([
            DB::raw("k.*"),
            DB::raw("csb.Name as GoodsName"),
            DB::raw("csb.BarCode"),
            DB::raw("csb.CommodityType"),
        ])
        ->join('commodityarchives_s2b as csb','csb.SKUCode','=','k.SkuCode');
        if( !empty($param['skuCode']) ) {
            $query = $query->where('k.SkuCode',$param['skuCode']);
        }
        
        if( isset($param['status']) ) {
            $status = (int)$param['status'];
            $query = $query->where('k.Status',$param['status']);
        }
        
        $pageInfo = $query->orderBy("k.id","desc")->paginate($limit,["*"],'page', $page,true)->toArray();
        foreach ($pageInfo['data'] as & $item)
        {
            $item['ID']                 = (string)$item['ID'];
            $item['GoodsName']          = filterReverseParam($item['GoodsName']);
            $item['SkuCode']            = (string)$item['SkuCode'];
            $item['NumberTotal']        = (string)$item['NumberTotal'];
            $item['NumberRemain']       = (string)$item['NumberRemain'];
            $item['StatusName']         = $item['Status'] == 1 ? '上架' : '下架';
            $item['CommodityTypeName']  = CommodityArchives::instance()->goods_types[$item['CommodityType']] ?? '未定义';
        }
        
        return Normal::returnCode(Normal::SUC,"获取成功",[
            'count' => $pageInfo['total'],
            'data'  => $pageInfo['data']
        ]);
    }
    
    /**
     * 库存减少服务功能
     * @param array $param
     */
    public function reduce(array $param)
    {
        if( empty($param['number']) ) {
            return Normal::returnCode(Normal::FAIL,"库存变化数量不能为空!");
        }
        
        if( empty($param['skuCode']) ) {
            return Normal::returnCode(Normal::FAIL,"变化商品条码不能为空!");
        }
        
        $number     = (int)$param['number'];
        $skuCode    = (string)$param['skuCode'];
        if( $number <= 0 ) {
            return Normal::returnCode(Normal::FAIL,"需要扣减的库存数量必须要大于0");
        }
        
        return $this->reduceByRedisSubscribe($skuCode, $number);
    }
    
    /**
     * 使用redis + publish + subscribe
     * @param string $skuCode
     * @param int $number
     */
    public function reduceByRedisSubscribe(string $skuCode,int $number)
    {
        try
        {
            $value = $this->execLuaSubscribeScript($skuCode, $number);
            if( $value < 0 ) {
                return Normal::returnCode(Normal::FAIL,"库存不足");
            }
            return Normal::returnCode(Normal::SUC,"库存扣减成功");
        } 
        catch ( \Throwable $th ) 
        {
            return Normal::returnCode(Normal::FAIL,"库存服务失败[{$th->getMessage()}]");
        }
    }
    
    
    /**
     * 使用mysql
     * @param string $skuCode
     * @param int $number
     */
    public function reduceByMysql(string $skuCode,int $number)
    {
        try
        {
            //使用mysql
            $stockInfo = CommodityStock::instance()->where("skuCode",$skuCode)->first();
            if( empty($stockInfo) ) {
                return Normal::returnCode(Normal::FAIL,"商品数据不存在");
            }
        
            //加锁
            $stock_id = $stockInfo['ID'];
            CommodityStock::instance()->where('ID',$stock_id)->lock(true)->first();
        
            //更新库存
            $result = CommodityStock::instance()->where('ID',$stock_id)
            ->where('NumberRemain','>=',$number)
            ->update([
                'NumberRemain'  => Db::raw("NumberRemain-{$number}"),
                'UpdateTime'    => getCommonDate()
                ]);
            if( ! $result ) {
                CommodityStock::instance()->rollback();
                return Normal::returnCode(Normal::FAIL,"库存不足");
            }
        
            $result = $this->addLog($skuCode, $number,self::stock_mode_order_decr);
            if( ! $result ) {
                CommodityStock::instance()->rollback();
                return Normal::returnCode(Normal::FAIL,"库存新增日志失败");
            }
            
            CommodityStock::instance()->commit();
            return Normal::returnCode(Normal::SUC,"操作成功");
        
        } catch ( \Throwable $th ) {
            CommodityStock::instance()->rollback();
            return Normal::returnCode(Normal::FAIL,"库存服务失败[{$th->getMessage()}]");
        }
    }

    /**
     * 使用redis + mq
     * @param string $skuCode
     * @param int $number
     */
    public function reduceByRedisAndMq(string $skuCode,int $number)
    {
        try 
        {
            $value = $this->execLuaScript($skuCode,$number);
            if( $value < 0 ) {
                return Normal::returnCode(Normal::FAIL,"库存不足!");
            }
            
            //rmq
            $param = [
                'skuCode'   => $skuCode,
                'number'    => $number
            ];
            $message = new StockRedisServiceMessageProducer($param);
            $producer = ApplicationContext::getContainer()->get(Producer::class);
            $result = $producer->produce($message,true);
            if( ! $result ) {
                return Normal::returnCode(Normal::FAIL,"库存服务功能异常");
            }
            return Normal::returnCode(Normal::SUC,"操作成功");
        } 
        catch ( \Throwable $th) 
        {
            return Normal::returnCode(Normal::FAIL,"库存服务失败[{$th->getMessage()}]");
        }
    }
    
    /**
     * 使用lua脚本执行redis操作
     * subscribe + publish
     */
    public function execLuaSubscribeScript(string $skuCode,int $number)
    {
        $redis_subscribe_queue = self::redis_subscribe_queue;
        // Lua脚本字符串
        $luaScript = <<<LUA
local key = KEYS[1]
local decrement = tonumber(ARGV[1])
local currentStock = tonumber(redis.call('get', key) or "0")
if currentStock < decrement then
    return -1
else
    redis.call('decrby', key, decrement)
    return redis.call('publish', '{$redis_subscribe_queue}', key .. '_' ..decrement)        
end
LUA;
        // 参数数组
        $params = [self::cache_redis_goods_key.$skuCode, $number];
        // 计算Lua脚本的SHA1校验和，以便缓存和重用
        $redis  = redis();
        $sha1   = $redis->script('load', $luaScript);
        $value  = $redis->evalSha($sha1,$params,1);
        return (int)$value;
    }    
    
    /**
     * 使用lua脚本执行redis操作
     */
    public function execLuaScript(string $skuCode,int $number)
    {
        // Lua脚本字符串
        $luaScript = <<<LUA
local key = KEYS[1]
local decrement = tonumber(ARGV[1])
local currentStock = tonumber(redis.call('get', key) or "0")
if currentStock < decrement then
    return -1
else
    return redis.call('decrby', key, decrement)
end
LUA;
        // 参数数组
        $params = [self::cache_redis_goods_key.$skuCode, $number];
        // 计算Lua脚本的SHA1校验和，以便缓存和重用
        $redis  = redis();
        $sha1   = $redis->script('load', $luaScript);
        $value  = $redis->evalSha($sha1,$params,1);
        return (int)$value;
    }
    
    /**
     * 初始化库存值
     * @param array $param
     */
    public function initStock(array $param)
    {
        if( empty($param['mode']) ) {
            return Normal::returnCode(Normal::FAIL,"库存变化模式不能为空,【1-增加，2-减少,3-全量初始化】");
        }
        
        if( empty($param['number']) ) {
            return Normal::returnCode(Normal::FAIL,"库存变化数量不能为空!");
        }
        
        if( empty($param['skuCode']) ) {
            return Normal::returnCode(Normal::FAIL,"变化商品条码不能为空!");
        }
        
        //如果是库存增加
        $mode       = $param['mode'];
        $number     = (int)$param['number'];
        $skuCode    = $param['skuCode'];
        $redis_goods_key = self::cache_redis_goods_key.$skuCode;
        $redis      = redis();
        
        //查询数据库
        try 
        {

            CommodityStock::instance()->startTrans();
            
            //全量初始化库存
            $stockInfo = CommodityStock::instance()->where("skuCode",$skuCode)->first();
            if( empty($stockInfo) ) 
            {
                if( $param['mode'] != self::stock_mode_init ) {
                    return Normal::returnCode(Normal::FAIL,"当前库存数据不存在，只能进行初始化功能!");
                }
                
                //初始化数据
                $data = [
                    'ID'            => IdService::instance()->getGoodsID(),
                    'SkuCode'       => $skuCode,
                    'NumberTotal'   => $number,
                    'NumberRemain'  => $number,
                    'CreateTime'    => getCommonDate()
                ];
                $result = CommodityStock::instance()->insert($data);
                if( ! $result ) {
                    CommodityStock::instance()->rollback();
                    return Normal::returnCode(Normal::FAIL,"库存初始化失败[001]");
                } 
                
                //写入日志
                $result = $this->addLog($skuCode, $number,self::stock_mode_init);
                if( ! $result ) {
                    CommodityStock::instance()->rollback();
                    return Normal::returnCode(Normal::FAIL,"库存初始化失败[002]");
                }
                
                $value = $redis->set($redis_goods_key,$number);
                if( ! $value ) {
                    CommodityStock::instance()->rollback();
                    return Normal::returnCode(Normal::FAIL,"库存初始化失败[003]");
                }
            }
            else
            {
                //增加库存
                if( $param['mode'] == self::stock_mode_change_incr ) 
                {
                    //更新库存
                    CommodityStock::instance()->where('ID',$stockInfo['ID'])->update([
                        'NumberTotal'   => Db::raw("NumberTotal+{$number}"),   
                        'NumberRemain'  => Db::raw("NumberRemain+{$number}"),
                        'UpdateTime'    => getCommonDate()
                    ]);
                    
                    //写入日志
                    $result = $this->addLog($skuCode, $number,self::stock_mode_change_incr);
                    if( ! $result ) {
                        CommodityStock::instance()->rollback();
                        return Normal::returnCode(Normal::FAIL,"库存新增失败[004]");
                    }
                    
                    $value = $redis->incrBy($redis_goods_key,$number);
                    if( ! $value ) {
                        CommodityStock::instance()->rollback();
                        return Normal::returnCode(Normal::FAIL,"库存新增失败[005]");
                    }
                }
                elseif ( $param['mode'] == self::stock_mode_change_decr ) {//减少库存
                    //更新库存
                    $result = CommodityStock::instance()->where('ID',$stockInfo['ID'])
                    ->whereRaw("`NumberTotal` >= `NumberRemain`")
                    ->where('NumberRemain','>=',$number)
                    ->update([
                        'NumberTotal'   => Db::raw("NumberTotal-{$number}"),
                        'NumberRemain'  => Db::raw("NumberRemain-{$number}"),
                        'UpdateTime'    => getCommonDate()
                    ]);
                    
                    if( ! $result ) {
                        CommodityStock::instance()->rollback();
                        return Normal::returnCode(Normal::FAIL,"库存减少失败[006],请核对数据是否满足要求，当前总库存必须要大于等于剩余库存!");
                    }
                    
                    $result = $this->addLog($skuCode, $number,self::stock_mode_change_decr);
                    if( ! $result ) {
                        CommodityStock::instance()->rollback();
                        return Normal::returnCode(Normal::FAIL,"库存减少失败[007]");
                    }
                    
                    $value = $redis->decrBy($redis_goods_key,$number);
                    $value = (int)$value;
                    if( $value < 0 ) {
                        $redis->incrBy($redis_goods_key,$number);
                        CommodityStock::instance()->rollback();
                        return Normal::returnCode(Normal::FAIL,"库存减少失败[008]，不能变更为负数");
                    }
                }
                else 
                {
                    CommodityStock::instance()->rollback();
                    return Normal::returnCode(Normal::FAIL,"库存不能再被初始化，请更换商品再进行新增操作!");
                }
            }
            
            CommodityStock::instance()->commit();
            return Normal::returnCode(Normal::SUC,"库存操作成功");
        } catch ( \Throwable $th ) {
            CommodityStock::instance()->rollback();
            return Normal::returnCode(Normal::FAIL,"库存服务失败[{$th->getMessage()}]");
        }
    }
    
    /**
     * 同步处理mysql数据库服务功能
     * @param string $skuCode
     * @param unknown $number
     */
    public function syncMysqlStockDecr($skuCode,$number)
    {
        $number = (int)$number;
        $stockInfo = CommodityStock::instance()->info(['SkuCode'=>$skuCode]);
        if( empty($stockInfo) ) {
            return Normal::returnCode(Normal::FAIL,"库存信息未找到");
        }
        
        //更新库存
        CommodityStock::instance()->startTrans();
        CommodityStock::instance()->where('ID',$stockInfo['ID'])->update([
            'NumberRemain'  => Db::raw("NumberRemain-{$number}"),
            'UpdateTime'    => getCommonDate()
        ]);
        
        //写入日志
        $result = $this->addLog($skuCode, $number,self::stock_mode_order_decr);
        if( ! $result ) 
        {
            CommodityStock::instance()->rollback();
            return Normal::returnCode(Normal::FAIL,"库存减少失败[004]");
        }
        
        CommodityStock::instance()->commit();
        return Normal::returnCode(Normal::SUC,"库存操作成功");
    }
    
    /**
     * 恢复库存数据
     * @param string $skuCode
     * @param unknown $number
     * 需要售后的人比较少，所以，这里 可以使用mysql进行恢复库存，并且双写到redis
     */
    public function recoverRedisAndMysqlStock(array $param)
    {
        if( empty($param['number']) ) {
            return Normal::returnCode(Normal::FAIL,"库存变化数量不能为空!");
        }
        
        if( empty($param['skuCode']) ) {
            return Normal::returnCode(Normal::FAIL,"变化商品条码不能为空!");
        }
        
        $number     = (int)$param['number'];
        $skuCode    = $param['skuCode'];
        if( $number <= 0 ) {
            return Normal::returnCode(Normal::FAIL,"需要恢复的库存数量必须要大于0");
        }
        
        try 
        {
            $stockInfo  = CommodityStock::instance()->info(['SkuCode'=>$skuCode]);
            if( empty($stockInfo) ) {
                return Normal::returnCode(Normal::FAIL,"库存信息未找到");
            }
            
            //更新库存
            CommodityStock::instance()->startTrans();
            $result = CommodityStock::instance()->where('ID',$stockInfo['ID'])->update([
                'NumberRemain'  => Db::raw("NumberRemain+{$number}"),
                'UpdateTime'    => getCommonDate()
            ]);
            if( ! $result ) {
                CommodityStock::instance()->rollback();
                return Normal::returnCode(Normal::FAIL,"库存恢复失败[001]");
            }
            
            //写入日志
            $result = $this->addLog($skuCode, $number,self::stock_mode_order_incr);
            if( ! $result ) {
                CommodityStock::instance()->rollback();
                return Normal::returnCode(Normal::FAIL,"库存恢复失败[002]");
            }
            
            //双写redis
            $redis = redis();
            $result = $redis->incrBy(self::cache_redis_goods_key.$skuCode,$number);
            if( ! $result ){
                CommodityStock::instance()->rollback();
                return Normal::returnCode(Normal::FAIL,"库存恢复失败[003]");
            }
            
            CommodityStock::instance()->commit();
            return Normal::returnCode(Normal::SUC,"库存操作成功");
        } 
        catch ( \Throwable $th )
        {
            CommodityStock::instance()->rollback();
            return Normal::returnCode(Normal::FAIL,"库存恢复失败[{$th->getMessage()}]");
        }
    }
    
    /**
     * 库存上架接口
     * @param array $param
     */
    public function stockUp(array $param)
    {
        if( empty($param['id']) ) {
            return Normal::returnCode(Normal::FAIL,"id不能为空");
        }
        
        if( !isset($param['status']) ) {
            return Normal::returnCode(Normal::FAIL,"status不能为空");
        }
        
        $_ID = $param['id'];
        $info = CommodityStock::instance()->info(['ID'=>$_ID]);
        if( empty($info) ) {
            return Normal::returnCode(Normal::FAIL,"库存数据不存在");
        }
        
        $status = $param['status'] == 1 ? 1 : 0 ;
        $result = CommodityStock::instance()->where('ID',$_ID)
        ->where('Status',$status == 1 ? 0 : 1)
        ->update([
            'Status'        => $status,
            'UpdateTime'    => getCommonDate()
        ]);
        
        if( !$result ) {
            return Normal::returnCode(Normal::FAIL,"操作失败，请核对状态是否正常!");
        }
        
        return Normal::returnCode(Normal::SUC,"操作成功");
    }
    
    /**
     * 添加日志
     * @param string $skuCode
     * @param int $number
     * @param int $mode
     */
    public function addLog(string $skuCode,int $number,int $mode) 
    {
        $data = [
            'ID'            => IdService::instance()->getGoodsID(),
            'Mode'          => $mode,
            'SkuCode'       => $skuCode,
            'Number'        => $number,
            'CreateTime'    => getCommonDate()
        ];
        return CommodityStockLog::instance()->insert($data);
    }
}